#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdarg.h>
#include <vector>

#pragma warning(disable: 4996)

#include "..\glew\glew.h"
#include "..\glut\glut.h"

#include "..\xobjIO\XobjIO.h"
#include "..\xobjIO\glMesh.h"

#include "..\cuCommon\cuTypes.h"

#include "..\rtCommon\RegularGrid.h"
#include "..\rtCommon\C_Camera.h"
#include "..\rtCommon\Plane.h"
#include "..\rtCommon\Frustum.h"
#include "..\rtCommon\BoolDebug.h"
#include "..\gui\C_Mouse.h"

#include "..\rtCommon\Intersect.h"

#include "..\glCommon\glCommon_string.h"

#include "..\glCommon\glCommon.h"
#include "..\glCommon\glShader.h"
#include "..\glCommon\glProgram.h"
#include "..\glCommon\glRender.h"

SBoolDebug g_bDebug;

std::vector<const char *> g_vPrintLog;
std::vector<C_Actor *>    g_vpXobj;
std::vector<CglMesh *>    g_vpMesh;

std::vector<CglShader *> g_vpVxShader;
std::vector<CglShader *> g_vpFtShader;

std::vector<CglProgram *> g_vpGlProgram;

C_Camera g_Camera;
C_Mouse  g_Mouse;

matrix4x4 view, proj;

namespace Gfx_n
{
    uint32 nWinW = 800;
    uint32 nWinH = 600;

    uint32 nPosX = 0;
    uint32 nPosY = 0;

    uint32 nFlags = GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH;
    uint32 nAA = 1;

    int32 nId = 0;

    bool bFullscreen = false;
};

bool ParseCommandLine(int argc, char *argv[])
{
    bool error = false;
    for (int iarg = 0; iarg < argc; iarg++)
    {
        if (strcmp(argv[iarg], GLUT_STRING_RESOLUTION) == 0)
        {
            if ( iarg+2 > argc ) 
            {
                fprintf(stderr, "Error: Invalid command line parameters, missing resolution values\n");
                error = true;
                break;
            }
            else
            {
                Gfx_n::nWinW = atoi(argv[iarg+1]);
                Gfx_n::nWinH = atoi(argv[iarg+2]);
            }
        }
        if (strcmp(argv[iarg], GLUT_STRING_POSITION) == 0)
        {
            if ( iarg+2 > argc ) 
            {
                fprintf(stderr, "Error: Invalid comand line parameters, missing position values\n");
                error = false;
                break;
            }
            else
            {
                Gfx_n::nPosX = atoi(argv[iarg+1]);
                Gfx_n::nPosY = atoi(argv[iarg+2]);
            }
        }
        if (strcmp(argv[iarg], GLUT_STRING_FULLSCREEN) == 0)
        {
            Gfx_n::bFullscreen = true;
        }
    }
    return error;
}


// Idle() is called every time in the message processing loop. It is the main callback function 
void Idle(void);
// Display() is called whenever window content is invalidated
void Display(void);
// Keyboard() is called whenever any of the character keys are pressed
void Keyboard(uint8 key, int32 mousex, int32 mousey);
// KeyboardUp() is called whenever any of the pressed character keys is released
void KeyboardUp(uint8 key, int32 mousex, int32 mousey);
// Special() is called whenever any of the special (F1-F12, etc) keys are pressed
void Special(int32 key, int32 x, int32 y);
// SpecialUp() is called whenever any of the pressed special (F1-F12, etc) keys is released
void SpecialUp(int32 key, int32 x, int32 y);

// Reshape() is called when window size is changed
void Reshape(int32 w, int32 h);

// Functions that process mouse behavior
// Mouse() is called whenever user clicks any button
void Mouse(int32 button, int32 state, int32 mousex, int32 mousey);
// Motion() is called whenever user holds any of the buttons while moving mouse
void Motion(int32 mousex, int32 mousey);
// PassiveMotion() is called whenever mouse moves and no buttons are pressed
void PassiveMotion(int32 mousex, int32 mousey);
// Entry() is called when mouse cursor enters / leaves current window
void Entry(int32 state);

int main(int argc, char *argv[])
{
  ParseCommandLine(argc, argv);

  glutInit( &argc, argv );    
  glutInitWindowSize(Gfx_n::nWinW, Gfx_n::nWinH);    
  glutInitWindowPosition(Gfx_n::nPosX, Gfx_n::nPosY);
  glutInitDisplayMode(Gfx_n::nFlags);

  Gfx_n::nId = glutCreateWindow("xobjRender test window");
  
  GLenum gl_error = glewInit();
  if (gl_error != GLEW_OK)
  {
    char glew_error[256];
    sprintf(glew_error, "Error: %s ", glewGetErrorString(gl_error));
    g_vPrintLog.push_back(glew_error);
  }
	else
	{
    char glew_status[256];
    sprintf(glew_status, "Status: Using GLEW %s ", glewGetString(GLEW_VERSION));
    g_vPrintLog.push_back(glew_status);
	}

  glutDisplayFunc    ( Display   );
  glutIdleFunc       ( Idle      );
  glutKeyboardFunc   ( Keyboard  );
  glutSpecialFunc    ( Special   );
  glutReshapeFunc    ( Reshape   );
  glutMouseFunc      ( Mouse     );
  glutMotionFunc     ( Motion    );

  if (Gfx_n::bFullscreen)
      glutFullScreen();  

  char * fname  = "..\\Media\\models\\test_scene2.XOBJ";
  //char * fname = "..\\Media\\models\\plane.XOBJ";
  //char * fname = "..\\Media\\models\\sphere2.OBJ";
  //char * fname = "..\\Media\\models\\pyramid.XOBJ";
  //char * fname = "..\\Media\\models\\teapot.XOBJ";
  //char * fname = "..\\Media\\models\\complex_scene.xobj";
  if (C_Actor::Load(g_vpXobj, fname ) != XOBJ_OK)
  {
    char xobj_error[512];
    sprintf(xobj_error, "Error: %s not found ", fname);
    g_vPrintLog.push_back(xobj_error);
  }
	else
  {
    char xobj_error[512];
    sprintf(xobj_error, "File %s loaded successfully. g_vpXobj.size() = %d ", fname, g_vpXobj.size());
    
    uint32 faceCount = 0;

    for (uint32 iobj = 0; iobj < g_vpXobj.size(); iobj++)
    {
      CglMesh * pMesh = new CglMesh(g_vpXobj[iobj][0]);

      pMesh->Update(0);

      g_vpMesh.push_back(pMesh);

      faceCount += g_vpXobj[iobj]->posIdx.size() / 3;

      g_vpXobj[iobj]->TransformWS();
    }
    
    printf("Number of faces per scene : %d\n", faceCount); 

    g_vPrintLog.push_back(xobj_error);
  }

  char * vs_fname[] = 
  { "..\\Media\\shaders\\simple.vs" };
  char * ps_fname[] = 
  { "..\\Media\\shaders\\simple.ps" };
  CglShader *pShader = NULL; 

  pShader = new CglShader();
  pShader->CreateFromFile(vs_fname[0], GL_VERTEX_SHADER);
  g_vPrintLog.push_back(pShader->m_Log);
  g_vpVxShader.push_back(pShader);

  pShader = new CglShader();
  pShader->CreateFromFile(ps_fname[0], GL_FRAGMENT_SHADER);
  g_vPrintLog.push_back(pShader->m_Log);
  g_vpFtShader.push_back(pShader);

  CglProgram *pProgram = new CglProgram();

  pProgram->Create(g_vpVxShader[0]->Id(), g_vpFtShader[0]->Id());
  g_vPrintLog.push_back(pProgram->m_Log);

  g_vpGlProgram.push_back(pProgram);

	float asp = Gfx_n::nWinW / (float) Gfx_n::nWinH;

	g_Camera.Asp(asp);
	g_Camera.Eye(f3(0, 1, 0));

  glEnable (GL_DEPTH_TEST);
  glDisable(GL_CULL_FACE);

  glutMainLoop();

  return 0;
}

void Reshape(int32 w, int32 h)
{
	float zn = g_Camera.zN();
	float zf = g_Camera.zF();
	float asp = w / (float) h;
	float fov = g_Camera.FoV();

  glViewport(0, 0, w, h);

  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  gluPerspective(fov, asp, zn, zf);

  g_Camera.Asp(asp); // update aspect ratio variable

  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();

  Gfx_n::nWinW = w;
  Gfx_n::nWinH = h;
}

void Display(void)
{
	float3 at = g_Camera.At();
	float3 eye = g_Camera.Eye();
	float3 up = g_Camera.Up();	

	float zn = g_Camera.zN();
	float zf = g_Camera.zF();
	float asp = g_Camera.Asp();
	float fov = g_Camera.FoV();

  glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();

  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  gluPerspective(fov, asp, zn, zf);

	glLookAt(eye, at, up);

  glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

  for (uint32 ixobj = 0; ixobj < g_vpXobj.size(); ixobj++)
  {         
      if (!g_bDebug.bDebug)
      {
          g_vpMesh[ixobj]->Update( 0 );
          glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
          g_vpGlProgram[0]->Enable();
          //glRender( g_vpXobj[ixobj][0] );       
          glRender( g_vpMesh[ixobj][0], false );
          g_vpGlProgram[0]->Disable();
      }      
  }
  
  glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

  float4 fontColor = {1.0f, 1.0f, 0.0f, 1.0f};
  glPrintf(-1.0f, 1.0f - ((20 + 30.0f) / Gfx_n::nWinH), &fontColor, GLUT_BITMAP_9_BY_15, "FPS = %f", glFPS(true, 100));

  for (uint32 ilog = 0; ilog < g_vPrintLog.size(); ilog++)
  {
      glPrintf(-1.0f, 1.0f - ((20 + 30.0f * (2 + ilog)) / Gfx_n::nWinH), &fontColor, GLUT_BITMAP_9_BY_15, g_vPrintLog[ilog]);
  }

  glutSwapBuffers();
}

void Idle(void)
{
    Display();
}

void Keyboard(uint8 key, int32 mousex, int32 mousey)
{
    switch (key)
    {
        case GLUT_KEY_ESC: 
            // any opengl resource clean-up should go here
            glutDestroyWindow(Gfx_n::nId);
            // any cpu resource clean-up should go here
            exit(0);
            break;
        case '0':
            g_bDebug.bDebug = !g_bDebug.bDebug;
            break;
        case '1':
            g_bDebug.bRenderMesh = !g_bDebug.bRenderMesh;
            break;
        case '2':
            g_bDebug.bRenderRegularGrid = !g_bDebug.bRenderRegularGrid;
            break;
        case '3':
            g_bDebug.bRenderAABB = !g_bDebug.bRenderAABB;
            break;
        case '4':
            g_bDebug.bRenderRegularGridCenter = !g_bDebug.bRenderRegularGridCenter;
            break;
        case '5':
            g_bDebug.bRenderFaceNormals = !g_bDebug.bRenderFaceNormals;
            break;

        default:
            break;
    }
}

void Special(int32 key, int32 mousex, int32 mousey)
{
    switch (key)
    {
        case GLUT_KEY_LEFT:
           g_Camera.Update((uint32) e_StrafeLeft, 0.5f);
        break;
        case GLUT_KEY_RIGHT:
            g_Camera.Update((uint32) e_StrafeRight, 0.5f);
        break;
        case GLUT_KEY_UP:
            g_Camera.Update((uint32) e_StepForward, 0.5f);
        break;
        case GLUT_KEY_DOWN:
            g_Camera.Update((uint32) e_StepBackward, 0.5f);
        break;

        default:
        break;
    }
}

void KeyboardUp(uint8 key, int32 mousex, int32 mousey)
{
    //switch (key)
    //{
    //    default:
    //    break;
    //}
}

void SpecialUp(int32 key, int32 mousex, int32 mousey)
{
    //switch (key)
    //{
    //    default:
    //    break;
    //}
}

void Mouse(int32 button, int32 state, int32 mousex, int32 mousey)
{
    g_Mouse.Update(mousex, mousey, button, !state);
}

void Motion(int32 mousex, int32 mousey)
{
    g_Mouse.Update(mousex, mousey);
		if (g_Mouse.Pressed((int32) e_Left))
    {
        int dx = g_Mouse.currX() - g_Mouse.prevX();
        int dy = g_Mouse.currY() - g_Mouse.prevY();

        g_Camera.Update(dx * 0.001f, -dy * 0.001f);
    }
}
